package com.sina.mysql.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.sina.mysql.advice.TransactionDefinitionInterceptor;
import com.sina.mysql.utils.*;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.*;
import org.springframework.core.env.Environment;
import org.springframework.orm.hibernate5.HibernateTemplate;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;

import javax.sql.DataSource;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import static com.sina.mysql.config.ContextConst.TX_MANAGER;
import static org.hibernate.validator.internal.util.Contracts.assertNotEmpty;
import static org.springframework.util.Assert.notNull;

/**
 * @ClassName DataSourceContextConfig
 * @Description:
 * @Author 段浩杰
 * @Date 2018/8/22 17:30
 * @Version 1.0
 */
@Configuration
@EnableConfigurationProperties({JpaProperties.class})
@Profile({"datasource", "dao"})
public class DataSourceContextConfig implements EnvironmentAware, ApplicationContextAware, InitializingBean {

    private static final Logger LOG = LoggerFactory.getLogger(DataSourceContextConfig.class);

    private Environment env;

    private ApplicationContext applicationContext;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.env = environment;
    }


    private Snowflake createSnowflake(final String startTimeStr) {
        // 获取服务器 主机 ip 列表
        String[] hostList = env.getProperty("jelly.server.hostList", String[].class, ArrayUtils.EMPTY_STRING_ARRAY);

        Set<String> serverHostSet = ArrayUtils.asUnmodifiableSet(hostList);
        assertNotEmpty(serverHostSet, "serverHostSet must not empty");

        // 获取主机 ip
        final String host = hostAddress();

        final boolean isServerHost = serverHostSet.contains(host);
        // 获取端口
        Integer port;

        if (isServerHost) {
            // 当是服务器时,端口配置在 云端
            port = env.getProperty(String.format("jelly.server.worker.%s", host), Integer.class);
        } else {
            port = 0;
        }

        final String dataCenterIdKey = String.format("jelly.server.data.center.%s", host);

        final String workerIdKey = String.format("jelly.server.worker.%s:%s", host, port);

        LOG.info("dataCenterIdKey:{},workerIdKey:{}:", dataCenterIdKey, workerIdKey);

        final Long defaultDataCenterId = isServerHost ? null : 0L;
        final Long defaultWorkerId = isServerHost ? null : 0L;

        final Long dataCenterId = env.getProperty(dataCenterIdKey, Long.class, defaultDataCenterId);

        notNull(dataCenterId, "dataCenterId required");
        final Long workerId = env.getProperty(workerIdKey, Long.class, defaultWorkerId);

        notNull(workerId, "workerId required");

        LOG.info("dataCenterId:{},workerId:{}", dataCenterId, workerId);

        final long startTime = Long.valueOf(startTimeStr);

        return new Snowflake(startTime, workerId, dataCenterId);
    }

    private String hostAddress() throws RuntimeException {
        try {
            InetAddress localHost = InetAddress.getLocalHost();
            //isFalse( localHost.isLoopbackAddress(), "host address can't is loop back" );
            return localHost.getHostAddress();
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 编号为 0 的主库
     */
    @Bean(initMethod = "init", destroyMethod = "close")
    public DruidDataSource master0DataSource() {
        return createMasterDataSource(true, 0);
    }

    /**
     * 编号为 0 的从库
     */
    @Bean(initMethod = "init", destroyMethod = "close")
    public DruidDataSource slaver0DataSource() {
        return createMasterDataSource(false, 0);
    }

    /**
     * 封装 数据源创建逻辑
     */
    private DruidDataSource createMasterDataSource(boolean master, int index) {
        String role = master ? "master" : "slaver";
        DruidDataSource ds = new DruidDataSource();

        ds.setUrl(env.getRequiredProperty(String.format("spring.datasource.%s.url[%s]", role, index)));
        ds.setUsername(env.getRequiredProperty(String.format("spring.datasource.%s.username[%s]", role, index)));
        ds.setPassword(env.getRequiredProperty(String.format("spring.datasource.%s.password[%s]", role, index)));
        ds.setDriverClassName(env.getRequiredProperty("spring.datasource.driver-class-name"));

        ds.setInitialSize(env.getProperty(String.format("spring.datasource.%s.initialSize[%s]", role, index), Integer.class, 2));
        ds.setMaxActive(env.getProperty(String.format("spring.datasource.%s.maxActive[%s]", role, index), Integer.class, 50));
        ds.setMaxWait(env.getProperty(String.format("spring.datasource.%s.maxWait[%s]", role, index), Long.class, 27L * 1000L));
        ds.setValidationQuery(env.getProperty(String.format("spring.datasource.%s.validationQuery[%s]", role, index), "SELECT @@SESSION.sql_mode"));

        ds.setTestOnBorrow(env.getProperty(String.format("spring.datasource.%s.testOnBorrow[%s]", role, index), Boolean.class, Boolean.FALSE));
        ds.setTestWhileIdle(env.getProperty(String.format("spring.datasource.%s.testWhileIdle[%s]", role, index), Boolean.class, Boolean.TRUE));
        ds.setTestOnReturn(env.getProperty(String.format("spring.datasource.%s.testOnReturn[%s]", role, index), Boolean.class, Boolean.FALSE));
        ds.setTimeBetweenEvictionRunsMillis(env.getProperty(String.format("spring.datasource.%s.timeBetweenEvictionRunsMillis[%s]", role, index), Long.class, 60L * 1000L));

        return ds;
    }

    /**
     * 路由数据库,负责将事务引入到具体的数据库
     */
    @Bean
    @Primary
    public DataSource mysqlRoutingDataSource() {
        MysqlRoutingDataSource dataSource = new MysqlRoutingDataSource();
        //默认数据源
        DataSource m0 = master0DataSource();
        dataSource.setDefaultTargetDataSource(m0);

        Map<Object, Object> map = new HashMap<>(4);
        map.put("m0", m0);
        map.put("s0", slaver0DataSource());

        dataSource.setTargetDataSources(map);
        return dataSource;
    }

    @Bean(name = TX_MANAGER)
    @Primary
    public HibernateTransactionManager txManager(@Qualifier("sessionFactory") SessionFactory sessionFactory, @Qualifier("mysqlRoutingDataSource") DataSource dataSource) {
        HibernateTransactionManager manager = new HibernateTransactionManager(sessionFactory);
        manager.setDataSource(dataSource);
        return manager;
    }


    /**
     * 会话工厂
     */
    @Bean
    @Primary
    public LocalSessionFactoryBean sessionFactory(@Qualifier("mysqlRoutingDataSource") DataSource dataSource
            , JpaProperties properties) throws IOException, ClassNotFoundException {
        // 设置 雪花算法
        SnowflakeIdentifierGenerator.setSnowFlake(createSnowflake(SnowflakeIdentifierGenerator.START_TIME));
        // 设置 雪花算法
        Snowflake snowflake = createSnowflake(UserBusinessIdentifierGenerator.START_TIME);
        UserBusinessIdentifierGenerator.setBigIntegerSequence(new BigIntegerSequence(snowflake));

        LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean();
        //此行代码是重要的基础设施,请不要随便修改
        MetadataBuilder metadataBuilder = factoryBean.getMetadataSources().getMetadataBuilder();
        metadataBuilder.applyBasicType(new CodeEnumUserType<>());

        ConfigHelper.configMetadataSources(applicationContext, metadataBuilder);

        factoryBean.setDataSource(dataSource);
        factoryBean.setPackagesToScan("com.sina.domain");

        Properties props = new Properties();
        props.putAll(properties.getProperties());
        props.put("hibernate.hbm2ddl.auto", properties.getHibernate().getDdlAuto());
        factoryBean.setHibernateProperties(props);

        return factoryBean;
    }

    @Bean
    @Primary
    public HibernateTemplate hibernateTemplate(@Qualifier("sessionFactory") SessionFactory sessionFactory) {
        HibernateTemplate hibernateTemplate = new HibernateTemplate();
        hibernateTemplate.setSessionFactory(sessionFactory);
        return hibernateTemplate;
    }

    @Bean
    @Description("用于在spring 事务增强之前获取事务定义,以便实现读写分离.")
    public TransactionDefinitionInterceptor transactionDefinitionInterceptor() {
        return new TransactionDefinitionInterceptor();
    }


    /**
     * @see #transactionDefinitionInterceptor()
     */
    @Bean
    public DefaultPointcutAdvisor transactionDefinitionPointcutAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("@annotation(org.springframework.transaction.annotation.Transactional)");

        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, transactionDefinitionInterceptor());
        advisor.setOrder(0);
        return advisor;
    }

    @Bean
    public StatementFactory statementFactory() {
        return new DefaultStatementFactory();
    }


    @Override
    public void afterPropertiesSet() throws Exception {

    }
}